//@author A0101200R

package logic;

import storage.ScheduleEvent;
import storage.Storage;
import storage.UndoRedoStackEntry;
import applogger.AppLogger;

public class ScheduleModify {
	
	// list of constants
	private static final String EMPTY_STRING = "";
	private static final String DATE_FLOATING_TASK = "99999999";

	// list of messages
	private static final String MSG_DATE = "d";
	private static final String MSG_EVENTNAME = "n";
	private static final String MSG_END_TIME = "et";
	private static final String MSG_START_TIME = "st";
	private static final String MSG_BY = " by ";
	private static final String MSG_TO = " to ";
	private static final String MSG_FROM = " from ";
	private static final String MSG_ON = " on ";
	private static final String MSG_WHITE_SPACE = " ";
	private static final String MSG_TASK_MODIFIED = "Successfully Modified task.";
	
	private static final String MSG_LOG_INVALID_ENDTIME = "In modify, there is a number format exception for end time.";
	private static final String MSG_LOG_INVALID_STARTTIME = "In modify, there is a number format exception for start time.";
	private static final String MSG_LOG_ILLEGAL_ARGUMENT = "In modify, there is a illegal argument exception error.";
	private static final String MSG_LOG_OUTOFBOUND_INPUT = "In modify, there is array index of bounds error.";
	private static final String MSG_LOG_NON_NUMERICAL_INPUT = "In modify, there is an number format exception error.";

	// list of commands
	private static final String COMMAND_DELETE = "del";
	private static final String COMMAND_MODIFY = "mod";
	private static final String COMMAND_ADD = "add";

	// list of error messages
	private static final String ERROR_INVALID_INDEX = "Error! The index you want to modify is invalid.";
	private static final String ERROR_NON_NUMERIC_INDEX = "Error! The index you have keyed in is not a number.";
	private static final String ERROR_NO_INDEX = "Error! You did not pass in a number.";
	private static final String ERROR_NULL_UID = "The UID should not be null.";
	private static final String ERROR_INVALID_PARAMETER = "The parameter you keyed in to modify the details is not valid.";
	private static final String ERROR_NO_START_TIME = "You did not enter a start time.";
	private static final String ERROR_NO_END_TIME = "You did not enter a end time.";
	private static final String ERROR_MSG_ADD_FAILED = "ERROR! Unable to add task!";
	private static final String ERROR_UNABLE_TO_MODIFY = "Task could not be modified";
	private static final String ERROR_PARAMETER_NO_INPUT = "You add in parameter but no new inputs.";
	private static final String ERROR_EMPTY_INPUT = "You did not input in any new inputs to modify tasks.";

	// list of variables from other classes
	Storage eventsList;
	ScheduleEvent taskToModify;
	ScheduleAdder taskToAdd;
	ScheduleUndoer undoer;
	StringBuilder addStringBuilder;

	// list of variables
	String newEventName = null;
	String newStartTime = null;
	String newEndTime = null;
	String newDate = null;
	String uid = null;
	
	// constructor
	public ScheduleModify(Storage eventsList, ScheduleAdder taskToAdd,
			ScheduleUndoer undoer) {
		this.eventsList = eventsList;
		this.taskToAdd = taskToAdd;
		this.undoer = undoer;
	}

	/**
	 * Modify the index that the user selects from the display screen to modify
	 * the details of the task selected. Users will input the details they want
	 * to modify, n for new eventName, st for new StartTime, et for new endTime,
	 * d for new date. Users can modify a few or all of the 4 categories, which
	 * the method will construct a new string containing the new details and
	 * call the ScheduleAdder class to add the new modified event while the
	 * method will delete the current event in the eventsList.
	 * 
	 * @param input
	 * @return MSG_TASK_MODIFIED /ERROR_NON_NUMERIC_INDEX /ERROR_NO_INDEX /ERROR_INVALID_INDEX
	 */
	public String modifyEvents(String input) {
		resetNewDetails();
		assert input != null;
		
		int index = -1;

		try {
			index = Integer.parseInt(input.split(MSG_WHITE_SPACE)[0]);

		} catch (NumberFormatException e) {
			AppLogger.log(MSG_LOG_NON_NUMERICAL_INPUT);
			return ERROR_NON_NUMERIC_INDEX;
			
		} catch (ArrayIndexOutOfBoundsException e) {
			AppLogger.log(MSG_LOG_OUTOFBOUND_INPUT);
			return ERROR_NO_INDEX;
		}

		int sizeOfList = eventsList.getSize();

		if (index <= 0 || index > sizeOfList) {
			return ERROR_INVALID_INDEX;
		}

		String output = processModify(input, index);
		if (!output.equals(EMPTY_STRING)) {
			undoer.undoLast();
			return output;
		}
		
		boolean isEmptyInput = input.equals(index + EMPTY_STRING);
		boolean hasNoNewName = input.equals(index + MSG_WHITE_SPACE + MSG_EVENTNAME);
		boolean hasNoNewDate = input.equals(index + MSG_WHITE_SPACE + MSG_DATE);
		boolean hasNoNewStartTime = input.equals(index + MSG_WHITE_SPACE + MSG_START_TIME);
		boolean hasNoNewEndTime = input.equals(index + MSG_WHITE_SPACE + MSG_END_TIME);
		
		if (isEmptyInput) {
			return ERROR_EMPTY_INPUT;
			
		} else if (hasNoNewName || input.equals(hasNoNewDate) 
				|| hasNoNewStartTime || hasNoNewEndTime) {
			return ERROR_PARAMETER_NO_INPUT;
			
		} else {
			return MSG_TASK_MODIFIED;
		}
	}
	
	/**
	 * Reset new details of a task.
	 */
	private void resetNewDetails() {
		this.newEventName = null;
		this.newStartTime = null;
		this.newEndTime = null;
		this.newDate = null;
		this.uid = null;
	}

	/**
	 * Delete the current task with the index in the list and proceed to process
	 * the new details of the modified task and add the new modified task into
	 * the list. Push a backup copy of the deleted old task onto the undo stack
	 * so the user can undo and retrieve back the old task. Clear the redo stack
	 * so it does not contain any other irrelevant tasks.
	 * 
	 * @param input
	 * @param index
	 * @return output or ERROR_UNABLE_TO_MODIFY
	 */
	protected String processModify(String input, int index) {
		taskToModify = eventsList.getEvent(index - 1);
		eventsList.deleteEvent(index);
		pushTaskToStack(COMMAND_DELETE, taskToModify);

		String output = modifyDetails(index, input);
		if (!output.equals(EMPTY_STRING)) {
			return output;
		}

		try {
			addModifiedEvent();
		} catch (IllegalArgumentException e) {
			AppLogger.log(MSG_LOG_ILLEGAL_ARGUMENT);
			return ERROR_UNABLE_TO_MODIFY;
		}

		eventsList.clearRedoStack();
		return output;
	}

	/**
	 * Push the task to the undo stack so user can retrieve the old task back if
	 * user types in something wrongly.
	 * 
	 * @param command
	 * @param event
	 */
	protected void pushTaskToStack(String command, ScheduleEvent event) {
		UndoRedoStackEntry newEvent = new UndoRedoStackEntry(command, event);
		eventsList.getUndoStack().push(newEvent);
	}

	/**
	 * Retrieve all the old informations of the event and the relevant keywords
	 * for modification to get the new information of the event and pass them to
	 * modifiedDetails to do the modification.
	 * 
	 * @param index
	 * @param input
	 * @return error or EMPTY_STRING
	 */
	protected String modifyDetails(int index, String input) {
		String oldEventName = taskToModify.getName();
		String oldDate = taskToModify.getDate();
		String oldStartTime = taskToModify.getStartTime();
		String oldEndTime = taskToModify.getEndTime();

		try {
			String[] keyWords = getDetails(index, input);
			int count = 1;

			for (int i = 1; i < keyWords.length; i += count) {
				count = 1;

				boolean isStartTime = keyWords[i].equals(MSG_START_TIME);
				boolean isEndTime = keyWords[i].equals(MSG_END_TIME);
				boolean isName = keyWords[i].equals(MSG_EVENTNAME);
				boolean isDate = keyWords[i].equals(MSG_DATE);
				
				String error = EMPTY_STRING;

				if (isStartTime) {
					newStartTime = keyWords[i + 1];
					error =  checkValidStartTime();
					count++;
					
				} else if (isEndTime) {
					newEndTime = keyWords[i + 1];	
					error = checkValidEndTime();
					count++;

				} else if (isName) {
					newEventName = EMPTY_STRING;
					while ((i + count) < keyWords.length
							&& !keyWords[i + count].equals(MSG_END_TIME)
							&& !keyWords[i + count].equals(MSG_START_TIME)
							&& !keyWords[i + count].equals(MSG_DATE)) {

						newEventName += keyWords[i + count] + MSG_WHITE_SPACE;
						count++;
					}
					newEventName.trim();

				} else if (isDate) {
					newDate = keyWords[i + 1];
					count++;
					
				} else {
					return ERROR_INVALID_PARAMETER;
				}
				
				if (!error.equals(EMPTY_STRING)) {
					return error;
				}
			}
			
		} catch (Exception e) {
			AppLogger.log(e + " in modifyDetails");
		}

		modifiedDetails(oldEventName, newEventName, oldDate, newDate,
				oldStartTime, oldEndTime, newStartTime, newEndTime);

		return EMPTY_STRING;
	}
	
	/**
	 * Check if the end time is a numerical value.
	 * 
	 * @return EMPTY_STRING or ERROR_NO_END_TIME
	 */
	protected String checkValidEndTime() {
		String error = EMPTY_STRING;
		
		try {
			Integer.parseInt(newEndTime);
			
		} catch (NumberFormatException e) {
			AppLogger.log(MSG_LOG_INVALID_ENDTIME);
			return ERROR_NO_END_TIME;
		}
		
		return error;
	}

	/**
	 * Check if the start time is a numerical value.
	 * 
	 * @return EMPTY_STRING or ERROR_NO_START_TIME
	 */
	protected String checkValidStartTime() {
		String isInt = EMPTY_STRING;
		
		try {
			Integer.parseInt(newStartTime);
			
		} catch (NumberFormatException e) {
			AppLogger.log(MSG_LOG_INVALID_STARTTIME);
			return ERROR_NO_START_TIME;
		}
		
		return isInt;
	}

	/**
	 * Creates a new StringBuilder to append all the relevant old or new
	 * information of the event.
	 * 
	 * @param oldEventName
	 * @param newEventName
	 * @param oldDate
	 * @param newDate
	 * @param oldStartTime
	 * @param oldEndTime
	 * @param newStartTime
	 * @param newEndTime
	 */
	protected void modifiedDetails(String oldEventName, String newEventName,
			String oldDate, String newDate, String oldStartTime,
			String oldEndTime, String newStartTime, String newEndTime) {
		addStringBuilder = new StringBuilder();

		modifiedName(oldEventName, newEventName);
		modifiedDate(oldDate, newDate, newStartTime, newEndTime);
		modifiedStartEndTime(oldStartTime, oldEndTime, newStartTime, newEndTime);
	}

	/**
	 * Get the details of what the user wants to modify without the index.
	 * 
	 * @param index
	 * @param input
	 * @return keyWords: an array consisting of the information the user wants to modify.
	 */
	protected String[] getDetails(int index, String input) {
		int startIndex = input.indexOf(new Integer(index).toString());

		String newParameters = input.substring(startIndex);
		String[] keyWords = newParameters.split(MSG_WHITE_SPACE);

		return keyWords;
	}
	
	/**
	 * Check if the new modified ScheduleEvent is valid as a task to be added.
	 * 
	 * @param input
	 * @return true or false
	 */
	private boolean checkValidInput(ScheduleEvent input) {
		if (input == null) {
			return false;
		}
		
		boolean gotStartTime = input.getStartTime() != null;
		boolean gotEndTime = input.getEndTime() != null;
		boolean noDate = input.getDate() == null;
		boolean isFloatingDate = input.getDate().equals(DATE_FLOATING_TASK);

		if ((noDate || isFloatingDate) && (gotStartTime || gotEndTime)) {
			return false;
		}

		if (gotStartTime && gotEndTime 
				&& input.getStartTime().compareTo(input.getEndTime()) > 0) {
			return false;
		}

		return true;
	}

	/**
	 * We will pass the new modified string with all the new and old details to
	 * ScheduleAdder class to add the event into the eventsList.
	 */
	protected void addModifiedEvent() throws IllegalArgumentException {
		String addString = addStringBuilder.toString();
		
		int commandType = taskToAdd.checkCommandType(addString);
		ScheduleEvent eventToAdd = taskToAdd.doCommand(commandType, addString);

		if (!checkValidInput(eventToAdd)) {
			throw new IllegalArgumentException();
		}

		setUID(eventToAdd);

		String addMsg = eventsList.addEvent(eventToAdd);

		if (addMsg.equals(ERROR_MSG_ADD_FAILED)) {
			throw new IllegalArgumentException();
		}

		pushTaskToStack(COMMAND_ADD, eventToAdd);
		pushTaskToStack(COMMAND_MODIFY, null);
	}
	
	/**
	 * Set the UID of the new modified ScheduleEvent to add.
	 * 
	 * @param eventToAdd
	 */
	protected void setUID(ScheduleEvent eventToAdd) {
		uid = taskToModify.getUID();
		if (uid == null) {
			throw new IllegalArgumentException(ERROR_NULL_UID);
		}
		eventToAdd.setUID(uid);
	}

	/**
	 * new StringBuilder will add in the eventName.
	 * 
	 * @param oldEventName
	 * @param newEventName
	 */
	protected void modifiedName(String oldEventName, String newEventName) {
		if (newEventName != null) {
			addStringBuilder.append(newEventName);
			
		} else {
			addStringBuilder.append(oldEventName);
		}
	}

	/**
	 * new StringBuilder will add in the eventDate.
	 * 
	 * @param oldDate
	 * @param newDate
	 */
	protected void modifiedDate(String oldDate, String newDate, String newStartTime, String newEndTime) {
		boolean gotNewDate = newDate != null;
		boolean gotNewStartTime = newStartTime != null;
		boolean gotNewEndTime = newEndTime != null;
		boolean oldDateIsFloating = oldDate.equals(DATE_FLOATING_TASK);
		
		if (gotNewDate) {
			addStringBuilder.append(MSG_ON + newDate);
			
		} else {
			
			if (!oldDateIsFloating) {
				addStringBuilder.append(MSG_ON + oldDate);
				
			} else if (gotNewStartTime || gotNewEndTime){
				addStringBuilder.append(MSG_ON + DATE_FLOATING_TASK);
				
			} else{
				addStringBuilder.append(EMPTY_STRING);
			}
		}
	}

	/**
	 * new StringBuilder will add in the eventStartTime and the eventEndTime.
	 * 
	 * @param oldStartTime
	 * @param oldEndTime
	 * @param newStartTime
	 * @param newEndTime
	 */
	protected void modifiedStartEndTime(String oldStartTime, String oldEndTime,
										String newStartTime, String newEndTime) {
		String newDetails = null;
		boolean hasNewStartEndTime = newStartTime != null && newEndTime != null;
		boolean noNewStartEndTime = newStartTime == null && newEndTime == null;
		boolean noOldStartEndTime = oldStartTime != null && oldEndTime != null;
		boolean hasOldEndTime = oldEndTime != null;
		boolean hasOldStartTime = oldStartTime != null;
		boolean hasNewEndTime = newEndTime != null;
		boolean hasNewStartTime = newStartTime != null;
		
		if (hasNewStartEndTime) {
			newDetails = MSG_FROM + newStartTime + MSG_TO + newEndTime;
			
		} else if (noNewStartEndTime) {
			if (oldStartTime == null && oldEndTime == null) {
				newDetails = EMPTY_STRING;
				
			} else if (noOldStartEndTime) {
				newDetails = MSG_FROM + oldStartTime + MSG_TO + oldEndTime;
				
			} else if (hasOldEndTime) {
				newDetails = MSG_BY + oldEndTime;
			}
			
		} else if (hasNewEndTime) {
			if (hasOldStartTime) {
				newDetails = MSG_FROM + oldStartTime + MSG_TO + newEndTime;
				
			} else {
				newDetails = MSG_BY + newEndTime;
			}
			
		} else if (hasNewStartTime) {
			if (hasOldEndTime) {
				newDetails = MSG_FROM + newStartTime + MSG_TO + oldEndTime;
			
			} else {
				newDetails = MSG_FROM + newStartTime;
			}
		}
		
		addStringBuilder.append(newDetails);
	}
}
